"""
__author__ = Zdenek Maxa

Classes holding details of communication transmitted between fdtcp and fdtd.

Implementations of actions - factual starting of FDT Java client/server
    processes by fdtd

Actions classes represent actions initiated from fdtcp, then shipped to
    remote party fdtd where some action instance settings are finalized
    and eventually performed.
    
Actions .execute() methods are called on the side of fdtd, so caller 
    as well as the conf object are the one associated with the fdtd
    remote party and not with local fdtcp.

"""

import os
import time
import random
import types
import datetime

from fdtcplib.utils.Executor import Executor, ExecutorException
from fdtcplib.utils.utils import getHostName, getDateTime, getRandomString, getUserName
from fdtcplib.common.errors import FDTDException, FDTCopyException



class CarrierBase(object):
    """Base class for all communication between fdtcp and FDTD service
       Base class for Actions, Result
    """
    def __init__(self, id):
        self.id = id


    
    def __str__(self):
        r = self.__class__.__name__
        for k, v in self.__dict__.items():
            if isinstance(v, types.ListType):
                r = "\t".join([r, " %s: list of %s items:\n" % (k, len(v))])
                for i in v:
                    r = "\t".join([r, " %s" % i])
            else:
                r = "\t".join([r, " %s: %s\n" % (k, v)])
        return r
    



class Action(CarrierBase):
    """Base class for all actions"""
    def __init__(self, id, timeout = None):
        CarrierBase.__init__(self, id)
        self.timeout = timeout


        
    def execute(self):
        m = "Base class (abstract), no implementation execute()"
        raise NotImplementedError, m
    
        


class TestAction(Action):
    """This action is used by client (fdtcp) to find out if
       daemon (fdtd service) is running and responding.
       This is the first communication between fdtcp and fdtd,
       id is generated here and used throughout the whole transfer process.
       Only timeout can be configurable for this type of action.
    """
    
    def __init__(self, timeout = None):
        id = self._getId()
        Action.__init__(self, id, timeout)


        
    def _getId(self):
        """Transfer job / request / id will consist of hostname of the
           machine fdtcp is invoked on and timestamp. This id will be used
           in all requests to fdtd for tracking the state of the transfer
           job, esp. by MonALISA ApMon.
           Transfers at fdtd will be associated with this ID - make it as
           unique as possible to avoid collisions.
        """
        h = getHostName()
        u = getUserName()
        dt = getDateTime()
        r = getRandomString('a', 'z', 5)
        id = "fdtcp-%s-%s-%s-%s" % (h, u, dt, r)
        return id
        
        
        
    def execute(self, **kwargs):
        r = Result(self.id)
        r.status = 0
        return r
    
    
    
    
class ReceivingServerAction(Action):
    def __init__(self, id, options):
        """Instance of this class is created by fdtcp and some parameters are
           passed (options), options is a dictionary.
        """        
        Action.__init__(self, id)
        self.options = options
        
        
    
    def _setUp(self, conf, port):
        # separate method for testing purposes, basically sets up command
        self.options["sudouser"] = self.options["gridUserDest"]
        self.options["port"] = port
        self.options["monID"] = self.id
        self.command = conf.get("fdtReceivingServerCommand") % self.options 
    
    
            
    def execute(self, conf = None, caller = None, apMon = None, logger = None):
        """This method is is called on the remote site where fdtd runs - here are
           also known all configuration details, thus final set up has to happen
           here.
        """
        startTime = datetime.datetime.now()
        # may fail with subset of FDTDException which will be propagated
        port = caller.getFreePort()
        self._setUp(conf, port)
        user = self.options["sudouser"]
        logger.debug("Local grid user is '%s'" % user)
        toWaitFor = conf.get("fdtServerLogOutputToWaitFor") % dict(port = port)
        toWaitTimeout = conf.get("fdtServerLogOutputTimeout")
        killTimeout = conf.get("fdtReceivingServerKillTimeout")
        executor = Executor(self.id, caller = caller, command = self.command,
                            blocking = False, port = port, userName = user, 
                            logOutputToWaitFor = toWaitFor, killTimeout = killTimeout,
                            logOutputWaitTime = toWaitTimeout, logger = logger)
        
        try:
            output = executor.execute()
        except ExecutorException, ex:
            m = ("Could not start FDT server on %s port: %s, reason: %s" %
                 (getHostName(), port, ex))
            caller.releasePort(port)
            logger.error(m)
            raise FDTDException(m)
        except Exception, ex:
            m = ("Could not start FDT server on %s port: %s, reason: %s" %
                 (getHostName(), port, ex))
            caller.releasePort(port)
            logger.error(m, traceBack = True)
            raise FDTDException(m)
        else:
            r = Result(self.id)
            r.status = 0
            # port on which FDT Java server runs
            r.serverPort = port
            r.msg = "FDT server is running"
            r.log = output
            logger.debug("Response to client: %s" % r)
            
            endTime = datetime.datetime.now()
            elapsed = (endTime - startTime).seconds
            par = dict(id = self.id, fdt_server_init = elapsed)
            logger.debug("Starting FDT server lasted: %s [s], logging with ApMon." % elapsed)
            if apMon:
                apMon.sendParameters("fdtd_server_writer", None, par)
            return r
    
    
    
    
class AuthServiceAction(Action):
    """Client (fdtcp) will issue this action in order to obtain port on
       which AuthService runs, then fdtcp starts AuthClient to perform authentication.
    """
    
    def __init__(self, id):
        Action.__init__(self, id)
        
        
        
    def execute(self, conf = None, caller = None, apMon = None, logger = None):
        """This method is is called on the remote site where fdtd runs.
        """
        r = Result(self.id)
        r.status = 0
        # set the port on which AuthService runs
        r.serverPort = conf.get("portAuthService")
        logger.debug("Response to client: %s" % r)
        return r
        
        
 

class SendingClientAction(Action):
    def __init__(self, id, options):
        """Instance of this class is created by fdtcp and some parameters are
           passed (options), options is a dictionary.
        """    
        Action.__init__(self, id)
        self.options = options
        
        
    
    def _setUp(self, conf):
        # generate FDT fileList
        fileListName = "/tmp/fdt-fileList-%s" % self.id
        try:
            fileList = open(fileListName, 'w')
        except IOError, ex:
            m = "Could not create FDT client fileList file %s, reason: %s" % (fileListName, ex)
            raise FDTDException(m) # this will be propagated to fdtcp
             
        for f in self.options["transferFiles"]: # is list of TransferFiles instances
            fileList.write("%s\n" % f)
        fileList.close()
        self.options["fileList"] = fileListName
        self.options["monID"] = self.id
        
        self.command = conf.get("fdtSendingClientCommand") % self.options

        
        
    def execute(self, conf = None, caller = None, apMon = None, logger = None):
        """This method is invoked by fdtd once the action object is received
           from remote fdtcp (where the action instance was created). Options
           known on fdtd are set on the action instance (e.g. finalizing command
           for invoking FDT Java - location of fdt.jar is known only at fdtd site).
        """
        # this method is called on PYRO service side, user its logger from now on
        
        localGridUser = self.options["gridUserSrc"]
        logger.debug("Local grid user is '%s'" % localGridUser)
        self.options["sudouser"] = localGridUser
        
        self._setUp(conf)
        killTimeout = conf.get("fdtSendingClientKillTimeout")
        executor = Executor(self.id, self.command, blocking = True, caller = caller,
                            userName = localGridUser, killTimeout = killTimeout,
                            logger = logger)
        
        try:
            output = executor.execute()
        except ExecutorException, ex:
            m = ("Could not start FDT client on %s, reason: %s" % (getHostName(), ex))
            logger.error(m)
            raise FDTDException(m)
        except Exception, ex:
            m = ("Could not start FDT client on %s, reason: %s" % (getHostName(), ex))
            logger.error(m, traceBack = True)
            raise FDTDException(m)
        else:
            # no exception was raised during execution (or handled well)
            r = Result(self.id)
            r.status = 0
            r.log = output
            r.msg = "Output from FDT client"
            return r
    
      
      

class AuthClientAction(Action):
    """This action is run purely locally, everything happens from fdtcp.
       AuthClient (Java) store the remote username into a file
       (fileNameToStoreRemoteUserName) and execute method reads it in
       and forwards this remote Grid user name to local caller (fdtcp)
       and deletes the file.
    """
    def __init__(self, id, options):
        Action.__init__(self, id)
        self.options = options



    def _setUp(self, conf, fileNameToStoreRemoteUserName):
        # separate method for testing purposes
        self.options["fileNameToStoreRemoteUserName"] = fileNameToStoreRemoteUserName
        # conf is local configuration object, i.e. of local fdtcp
        self.options["x509userproxy"] = conf.get("x509userproxy")
        self.command = conf.get("authClientCommand") % self.options

        
    
    def execute(self, conf = None, caller = None, apMon = None, logger = None):
        """This method is invoked by fdtcp (locally)."""
        # fileNameToStoreRemoteUserName - file into which AuthClient stores name of the
        # Grid user at the remote party, this information is then forwarded later to
        # fdtd which doens't have to do the user mapping lookup again
        fileName = "/tmp/" + self.id + "--" + getRandomString('a', 'z', 5)
        if os.path.exists(fileName):
            raise FDTCopyException("File %s exists." % fileName)
        
        self._setUp(conf, fileName)
        executor = Executor(self.id, self.command, blocking = True, logger = logger)
        # here the Java AuthClient stores the Grid user name into the file
        output = executor.execute()
        
        try:
            remoteGridUser = open(fileName, 'r').read()
            os.remove(fileName)
        except Exception, ex:
            m = "Problem handling file %s (reading remote Grid user name), reason: %s" % (fileName, ex)
            raise FDTCopyException(m)
            
        # no exception was raised during execution (or handled well)
        r = Result(self.id)
        # TODO
        # this really reliable as authentication result?
        r.status = executor.returncode
        r.log = output
        return r, remoteGridUser



class CleanupProcessesAction(Action):
    def __init__(self, id, timeout = None):
        """id is mandatory - associated with previous."""
        Action.__init__(self, id, timeout)
    
        
    
    def execute(self, conf = None, caller = None, apMon = None, logger = None):
        caller.killProcess(self.id, logger)
        r = Result(self.id)
        r.status = 0
        r.msg = "No errors caught during process cleaning %s" % self        
        return r
        
         
        
        
class Result(CarrierBase):
    def __init__(self, id):
        """Result object always, id associated with previously launched action."""
        CarrierBase.__init__(self, id)
        self.log = None
        self.msg = None
        self.status = None
        self.host = getHostName()
        self.serverPort = None
    
        
    def __str__(self):
        n = self.__class__.__name__
        r = ("%s: %s id: '%s' status: '%s' msg: '%s' " % (n, self.host, self.id,
                                                          self.status, self.msg))
        return r